﻿using System;
using System.Data;
using System.IO;

namespace SVF
{
    /// <summary>
    /// Class for a separated values file from file system.
    /// The row seperator, the column seperator and
    /// the string encloser may be specified.
    /// Row and column seperators will be stripped during the
    /// parsing process.
    /// </summary>
    public class SeparatedValuesFile
    {
        private readonly string _rowSeparator = String.Empty;
        private readonly string _columnSeparator = String.Empty;

        private readonly DataTable _data = new DataTable();

        /// <summary>
        /// Creates an instance with given row and column separators.
        /// </summary>
        /// <param name="rowSeparator">The string to use as separator for rows in the separated values file</param>
        /// <param name="columnSeparator">The string to use as separator for columns in the separated values file</param>
        /// <exception cref="ArgumentNullException">Thrown if either the row or the column separator is null or an empty string</exception>
        /// <exception cref="ArgumentException">Thrown if the column Separator is the same string as the row separator</exception>
        public SeparatedValuesFile(string rowSeparator, string columnSeparator)
        {
            if (String.IsNullOrEmpty(rowSeparator)) throw new ArgumentNullException("rowSeparator", Properties.Resources.ArgumentNullOrEmptyException);
            if (String.IsNullOrEmpty(columnSeparator)) throw new ArgumentNullException("columnSeparator", Properties.Resources.ArgumentNullOrEmptyException);
            if (columnSeparator.IndexOf(rowSeparator, StringComparison.Ordinal) > -1) throw new ArgumentException(Properties.Resources.RowNoSubStringOfColSeparator, "rowSeparator");

            this._rowSeparator = rowSeparator;
            this._columnSeparator = columnSeparator;
        }

        /// <summary>
        /// Loads data from a given file. The reading and the processing of the
        /// data happens parallel. The method returns when both processes completed.
        /// So after the method returns the file was completely loaded
        /// </summary>
        /// <param name="fileName">The absolute path to the file to load</param>
        /// <exception cref="System.ArgumentNullException">Thrown if no file name is specified</exception>
        /// <exception cref="System.ArgumentException">Thrown if the file does not exist or cannot be read</exception>
        public virtual void Load(string fileName)
        {
            if (String.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName", Properties.Resources.ArgumentNullOrEmptyException);
            if (!File.Exists(fileName)) throw new ArgumentException(Properties.Resources.FileDoesNotExistException, fileName, new FileNotFoundException(Properties.Resources.FileDoesNotExistException, fileName));

            string fileData;

            try
            {
                // The StreamReader class takes care of the encoding
                using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                using (var ts = new StreamReader(fs, true))
                {
                    fileData = ts.ReadToEnd();

                    ts.Close();
                }
            }
            catch (Exception ex)
            {
                throw new ArgumentException(Properties.Resources.FileNotReadableException, fileName, ex);
            }

            // Empty rows are kept. As a side effect there is now an emty row at most of the test files.
            var rows = fileData.Split(new[] {this._rowSeparator}, StringSplitOptions.None);

            foreach (var row in rows)
            {
                // Everything okay here. The code works as expected when emty rows are kept.
                var columns = row.Split(new[] {this._columnSeparator}, StringSplitOptions.None);
                var missingRowCount = columns.Length - this._data.Columns.Count;

                for (var i = 0; i < missingRowCount; i++) this._data.Columns.Add(new DataColumn());

                this._data.Rows.Add(columns);
            }
        }

        /// <summary>
        /// Gets the number of rows available
        /// </summary>
        public int Rows
        {
            get { return this._data.Rows.Count; }
        }

        /// <summary>
        /// Gets the row at a specified index as array of columns
        /// </summary>
        /// <param name="index">The zero based index of the row to get</param>
        /// <returns>The columns of the row</returns>
        /// <exception cref="System.IndexOutOfRangeException">Thrown if there is no row at the given index</exception>
        public string[] this[int index]
        {
            get
            {
                // Index range is implicitly checked when accessing the row.
                // The exception is simply rethrown
                var row = this._data.Rows[index];
                var strColumns = new string[row.ItemArray.Length];

                for (var i = 0; i < row.ItemArray.Length; i++) strColumns[i] = row.ItemArray[i].ToString();

                return strColumns;
            }
        }
    }
}